package com.ilikesoup.instant.monitor.core;

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Stream;

public class DefaultStringSerializer implements Function {

    public static final DefaultStringSerializer INSTANCE = new DefaultStringSerializer();

    @Override
    public String apply(Object o) {
        if (o == null) {
            return "null";
        }
        Class<?> clazz = o.getClass();
        if (clazz == Object[].class || clazz.isArray()) {
            return handleArray(o);
        } else if (Collection.class.isAssignableFrom(clazz)) {
            return handleCollection(o);
        } else if (Map.class.isAssignableFrom(clazz)) {
            return handleMap(o);
        } else if (clazz.isPrimitive() || o instanceof Number || clazz == String.class || clazz == Character.class || clazz == Boolean.class) {
            return "(" + clazz.getSimpleName() + ")" + o;
        } else if (Date.class.isAssignableFrom(clazz)) {
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS XXX").format(o);
        } else if (notOverrideToString(clazz)) {
            return handleModel(o);
        }
        return o.toString();
    }

    private String handleModel(Object obj) {
        StringBuilder sb = new StringBuilder();
        sb.append("{");
        try {
            BeanInfo beanInfo = Introspector.getBeanInfo(obj.getClass(), Object.class);
            PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
            joinEach(Arrays.stream(propertyDescriptors),one -> sb.append(", "), one -> {
                Method readMethod = one.getReadMethod();
                if (readMethod != null) {
                    try {
                        if (!readMethod.isAccessible()) {
                            readMethod.setAccessible(true);
                        }
                        Object val = readMethod.invoke(obj);
                        sb.append(one.getName()).append("=").append(apply(val));
                    } catch (IllegalAccessException | InvocationTargetException e) {
                        e.printStackTrace();
                    }
                }
            });
        } catch (IntrospectionException e) {
            e.printStackTrace();
        }
        sb.append("}");
        return sb.toString();
    }

    private boolean notOverrideToString(Class<?> clazz) {
        try {
            return clazz.getMethod("toString").getDeclaringClass() == Object.class;
        } catch (NoSuchMethodException e) {
            // 不会发生
            throw new RuntimeException(e);
        }
    }

    private String handleMap(Object o) {
        Map<?, ?> map = (Map<?, ?> ) o;
        StringBuilder sb = new StringBuilder();
        sb.append("{");
        joinEach(map.entrySet().stream(), entry -> sb.append(", "),
                entry -> sb.append(apply(entry.getKey())).append("=").append(apply(entry.getValue())));
        sb.append("}");
        return sb.toString();
    }

    private String handleCollection(Object o) {
        Collection<?> args = (Collection<?>) o;
        StringBuilder sb = new StringBuilder();
        sb.append("[");
        joinEach(args.stream(), one -> sb.append(", "), one -> sb.append(apply(one)));
        sb.append("]");
        return sb.toString();
    }

    private String handleArray(Object o) {
        Object[] args = (Object[]) o;
        StringBuilder sb = new StringBuilder();
        sb.append("[");
        joinEach(Arrays.stream(args),one -> sb.append(", "), one -> sb.append(apply(one)));
        sb.append("]");
        return sb.toString();
    }

    private <T> void joinEach(Stream<T> stream, Consumer<T> notFirst, Consumer<T> each) {
        BooleanWrapper first = new BooleanWrapper(true);
        stream.forEach(t -> {
            if (!first.getBool()) {
                notFirst.accept(t);
            }
            each.accept(t);
            first.setBool(false);
        });
    }

    private static class BooleanWrapper {
        boolean bool;

        public BooleanWrapper(boolean bool) {
            this.bool = bool;
        }

        public boolean getBool() {
            return bool;
        }

        public void setBool(boolean bool) {
            this.bool = bool;
        }
    }

}
